Previous Page TOC Next Page


4 — Timing Is Everything

by Paul W. Logston

Many programs have to respond to time in one form or another. Some programs measure time—they time things. An example of this type of program is one that monitors the time it takes other programs (or parts of programs) to run. Other programs are controlled by time—they exist to respond to "time events" external to the program itself. An example of this type of program is a scheduler.

As the remainder of this chapter demonstrates, Visual Basic 4.0 has facilities to easily create both types of programs without resorting to external C functions or Windows SDK calls.

Timing Things

The ability to measure time is an important part of any programming language. For example, games depend heavily on timing to pace the action so that they don't run too fast. Even business applications use delays to pace the application. In the technical arena, the ability to measure time is what makes code profilers possible.

Fortunately, Visual Basic provides a handy way to measure time: the built-in Timer function. The operation of the Timer function is simple: it returns the number of seconds since midnight. Although this is an arbitrary number and is not very useful to look at, it provides a way to mark a point in time. For example, if you call Timer, store the value it returns, and later call the function again, the difference between the two values is the elapsed time in seconds between the two calls. This simple formula is the basis for all timing done with the Timer function.

A Simple Sleep Subroutine

Probably the most simple, and common, use for Timer is when you need to insert a timed delay into your program. Back in the old days of GWBASIC, delays were often inserted into programs with empty FOR...NEXT loops like the following one:

FOR I = 1 TO 1000

NEXT I

This method is actually a poor way to insert delays. Its main flaw is that the actual amount of time it delays the program is unpredictable because it depends totally on the speed of the computer it is running on. In addition, you cannot use this method to set a specific delay even when you know which machine you are using unless you do a lot of tinkering to get the number of iterations to match the desired delay. You are likely to have to play with the number of iterations several times and may end with code like FOR I = 1 TO 1127 to set a delay of one second—at least on your machine.

With the Timer function, your delays can be a little more precise. A simple example of a Timer-based function that delays a program for a number of seconds is as follows:

Sub Sleep(sngNumberOfSeconds As Single)

    Dim sngEndTime As Single

        ' compute the time until the end of the delay

    sngEndTime = Timer + sngNumberOfSeconds

        'Spin until the time is up

    Do

    Loop Until Timer >= sngEndTime

End Sub

This subroutine does what was advertised—you call it and it comes back the specified number of seconds later. But it has some problems. If you run this subroutine in the 16-bit versions of Windows (Windows 3.x), you will notice that nothing else in the system responds during the delay loop. This is caused by the lack of a DoEvents function call inside the Do...Loop. Without a DoEvents call, nothing else in Windows can happen because that function is the heart of the cooperative multitasking architecture of 16-bit Windows. Cooperative multitasking depends on applications to be "nice" and release control so that other applications can have some processor time. The DoEvents function is the way Visual Basic applications can fulfill this requirement to be "nice" to other applications. Note that releasing time to other applications is automatic when you are just waiting for user input; however, when you are running in a tight loop, like the preceding subroutine, DoEvents is required. The revised Sleep subroutine would look like this:

Sub Sleep(sngNumberOfSeconds As Single)

    Dim sngEndTime As Single

        ' compute the time until the end of the delay

    sngEndTime = Timer + sngNumberOfSeconds

        ' Spin until the time is up

    Do

        DoEvents    ' Let other things happen

    Loop Until Timer >= sngEndTime

End Sub

Note

If you have experience with Windows SDK, note that the DoEvents function is similar to the Windows SDK Yield function.

Even in the 32-bit environments like Windows NT or Windows 95, the DoEvents function serves a purpose. Although you don't have the problem of locking up other applications by not releasing time (both Windows NT and Windows 95 preemptively multitask 32-bit applications), your applications do not receive or process any events while you are in a tight loop. Use DoEvents to allow your application to respond to events during the delay.

The next problem with the simple Sleep subroutine shown earlier is something the documentation and the help system don't really make clear: the Timer function resets at midnight. If you begin timing at 11:59 p.m. and want to wait for 180 seconds (3 minutes), the Sleep subroutine never returns. Although this isn't obvious, it becomes evident when you walk through the code:

  1. At the beginning of the subroutine, Timer returns 86,340 (11:59 p.m. is 86,340 seconds from the last midnight).

  2. When you add the desired delay time (180) to the initial timer value (86,340), you get the anticipated end value (86,520).

  3. The subroutine loops until Timer returns a value greater than the end value of 86,520. But this condition can never be met because the maximum number of seconds in a day is 86,400. Timer can never return a value greater than 86,400. Thus, you are waiting for something that can never happen, and the loop continues to run endlessly.

In fact, any delay that goes past midnight suffers from this problem. This bug becomes even more problematic if you forget to call DoEvents in the loop: your application (and even the entire system under Windows 3.x) stops responding during that endless loop.

A More Complete Sleep Subroutine

The problem of timing past midnight is easily solved with a little extra code. Following is a revised Sleep subroutine that shows how to account for the passing of midnight:

Sub Sleep(sngNumberOfSeconds As Single)

    Const SECONDS_IN_DAY = 86400!

    Dim sngStartTime As Single, sngEndTime As Single, sngCurrentTime As Single

        ' setup the timer start and end times

    sngStartTime = Timer

    sngEndTime = sngStartTime + sngNumberOfSeconds

        ' spin until the time has elapsed

    Do

        DoEvents

            ' if the current time is less than the start time, then we know that

            ' we've passed midnight.  If that happens, apply an adjustment

            ' so the values will compare correctly.

        sngCurrentTime = Timer

        If sngCurrentTime < sngStartTime Then

            sngCurrentTime = sngCurrentTime + SECONDS_IN_DAY

        End If

    Loop Until sngCurrentTime >= sngEndTime

End Sub

The preceding Sleep subroutine is more complex than the simple, but flawed, one shown earlier. However, the subroutine is readily understandable when you look at it in detail:

  1. The subroutine records the start time of the process and computes the desired end time.

  2. The subroutine loops; on each iteration, it determines whether the end time has passed. To do this, you must see whether Timer has reset itself (you do this by seeing whether the current value of Timer is less than the value recorded when the process started). If this is true, you know that midnight has passed; you then adjust the current value by adding a day's worth of seconds so that the value will compare correctly with the computed end time. If you haven't passed midnight, you just compare the values without modification.

Note that during the loop, the subroutine references Timer only once, storing its value in a variable for later use. Why? Besides some minor speed improvements, the subroutine has to store the Timer value because the value returned by Timer may change between the different calls. By storing the value in a program variable, you can ensure that the value is always consistent during the subroutine's computations and comparisons.

The Granularity of the Timer Function

In the Sleep subroutine used in the preceding section, notice that all Timer-related variables are declared as single-precision numbers. Why is that, when Timer simply returns the number of seconds since midnight? This is another fact that the documentation doesn't clearly bring out: the Timer function actually returns a fractional number of seconds. By using single-precision numbers to compute the delay, you can use the Timer function to time a fractional number of seconds. Because Sleep uses single-precision variables, all the following calls to Sleep are valid:

Sleep 10

Sleep .5

Sleep 2.78

You can demonstrate for yourself that Timer returns a fractional number of seconds by using the small program shown in Figure 4.1. The example is simple—click anywhere on the form, and the program prints the next 10 values returned by the Timer function as fast as it can. Most of the values printed are the same because the loop runs fast enough to print several Timer values before Timer increments. But you should see the Timer function increment in a few of those values.


Figure 4.1. Demonstrating Timer resolution.

Take note of a curious thing: the Timer values increment in steps of .03 or .04 instead of .01. This increment is the granularity of the Timer function (that is, the smallest amount of time Timer can measure). Keep this granularity in mind when you write programs based on the Timer function; the increment may not be as fine as you expect.


Note

Don't put too much faith in Timer's granularity as demonstrated in Figure 4.1. If you include a DoEvents call in the timing loop, you can never predict how much time DoEvents will take. Depending on the number of messages queued up in Windows to be processed, DoEvents may return immediately or may return minutes later. The timing granularity may also vary based on the hardware platform used. Although this is currently a concern only for Windows NT, any assumptions may limit the portability of your application.

Measuring Time

As you saw in the first part of this chapter, an important use of the Timer function is to insert delays in a program. However, a delay is simply a piece of code that continually measures the time elapsed from its start to the desired end time. Using similar concepts, you can use the Timer function to time things other than a simple delay loop. The process is conceptually very simple: record the value returned by Timer, perform the process you want to time, and record a second Timer value. The difference between the two values is the number of seconds that elapsed while the process was being performed.

The two calls to Timer can be easily encapsulated into a pair of function calls—one to start the timer and another to calculate the difference and return it. With a little enhancement on the basic concept, you can create a useful program profiler, like the one shown in Listing 4.1.

Option Explicit

#If PROFILING Then

    Const MAX_SECONDS = 86400   ' number of seconds at midnight

    Dim iFileHandle As Integer  ' handle for the output file

    Dim sngTimers(20) As Single ' storage for 20 simultaneous timers

    Dim iLastTimer As Integer   ' last entry in the array used

#End If

Function StartProfiler(sFileName As String) As Integer

#If PROFILING Then

    On Error GoTo Error_StartProfiler

        ' open an output file for the timing information

    iFileHandle = FreeFile

    Open sFileName For Output As #iFileHandle

        ' initialize the counters

    iLastTimer = 0

    StartProfiler = True

    Exit Function

Error_StartProfiler:

    StartProfiler = False

    Exit Function

#Else

    StartProfiler = True

    Exit Function

#End If

End Function

Function StopProfiler() As Integer

#If PROFILING Then

    On Error GoTo Error_StopProfiler

        ' close the output file.

    Close #iFileHandle

    StopProfiler = True

    Exit Function

Error_StopProfiler:

    StopProfiler = False

    Exit Function

#Else

    StopProfiler = True

    Exit Function

#End If

End Function

Function MarkStartTime() As Integer

#If PROFILING Then

    On Error GoTo Error_MarkStartTime

    Dim i As Integer

        ' get the next entry to use

    i = iLastTimer + 1

        ' record the starting time value

    sngTimers(i) = Timer

    iLastTimer = i

    MarkStartTime = True

    Exit Function

Error_MarkStartTime:

    MarkStartTime = False

    Exit Function

#Else

    MarkStartTime = True

    Exit Function

#End If

End Function

Function MarkEndTime(sTag As String) As Integer

#If PROFILING Then

    On Error GoTo Error_MarkEndTime

    Dim sngEndTime As Single, sngDifference As Single

        ' record the end time and adjust for the passage of midnight

    sngEndTime = Timer

    If sngEndTime < sngTimers(iLastTimer) Then sngEndTime = sngEndTime + _MAX_SECONDS

        ' compute the elapsed time

    sngDifference = sngEndTime - sngTimers(iLastTimer)

        ' write it to a file, with a number of spaces that reflect

        ' its place in the stack of timers.

    Print #iFileHandle, Spc(iLastTimer); sTag; " "; sngDifference

        ' free an array entry for re-use

    iLastTimer = iLastTimer - 1

    MarkEndTime = True

    Exit Function

Error_MarkEndTime:

    MarkEndTime = False

    Exit Function

#Else

    MarkEndTime = True

    Exit Function

#End If

End Function

The Profiling Functions

The profiler example in Listing 4.1 works on a stack concept—multiple timers can be started, but they must be stopped in the reverse of their starting order. The reason you use multiple timers is simply so that you can time at multiple levels: You can measure the time it takes for an entire process to run and also measure the time individual portions of that process consume. The functions also record the timing information to a program-specified file rather than to any kind of window so that the functions can be inserted and have no impact on the actual user interface of the program (except for whatever speed impact they may add).

The profiling functions are globally enabled or disabled using Visual Basic 4.0's conditional compilation options. By setting the PROFILING compilation constant to a nonzero value, you enable the profiling code. When the profiling is disabled (PROFILING is either not defined or is defined as zero), all the functions return TRUE immediately and do nothing else. By immediately returning when they are not enabled, the profiling functions should have minimal impact on your program's speed. This arrangement means that you can insert the profiling functions in your application and simply disable them when you do the production compile, rather than going though your code to remove them manually.


Note

Conditional Compilation is a new feature in Visual Basic 4.0. Conditional Compilation works like a normal IF...THEN statement, except that the statement is evaluated when the executable is made and not at runtime. Since the condition is not evaluated when the program runs, there is no performance overhead added to the program. In fact, the false side of the #IF...#END IF doesn't even get included in the compiled program. Note that since the Conditional Compilation directives are evaluated at compile time, you can't refer to any program variables in them—their values just aren't known until the program runs!

Conditional Compilation is normally used to insert debugging code that you want to remove in the production program, or to create multiple versions of a program from the same set of source code, like versions for multiple platforms.

To add the profiler to your project, include the PROFILE.BAS module and set PROFILING=1 in the Conditional Compilation Arguments field in the Tools | Options window under the Advanced tab. Then simply insert the calls to the profiling functions in the sections of code where you want timing information, as the example in Figure 4.2 shows.


Figure 4.2. Using the profiling functions.

To use the profiling functions, insert the StartTimer function in your code before the first time you want to begin making profiling calls. All StartTimer does is to open the specified output file (which holds the profiling information) and to set the counter for the last timer, iLastTimer, to zero so that you begin fresh in the array. The StopTimer function is a cleanup function that closes the timing output file and returns.

The MarkStartTime and MarkEndTime functions are the ones that actually do the timing. MarkStartTime allocates a timer value from the sngTimers array and sets it to the current value of the Timer function. MarkEndTime simply computes the elapsed time based on the last entry in the sngTimers array and writes the time difference to a file, along with any programmer-specified text passed into the function. MarkEndTime then removes the used timer from the array by decrementing iLastTimer.

When you run your program, the profiling information is silently written to the file you specified so that you can examine it at your leisure.

Timing Processes with Now

There is an inherent problem with routines based on the Timer function: because Timer resets every 24 hours, you can't use it to time anything greater than 24 hours. Also, although the values returned by Timer are fine for computing the raw number of seconds a process takes, these values force the programmer to convert by hand if information is required in minutes or seconds. If these limitations are unacceptable for your purposes, consider building your timers based on the Now function.

Now returns a Date type instead of a simple number of seconds. The good news is that, because it is a Date type, you can time things for any number of days (or months and years). The bad news is that, because it is a Date type, doing computations and displaying the timing information is a little more complex: you have to use the special Visual Basic date functions.

The easiest way to get the difference between two Date values is to use the DateDiff function. With DateDiff, you can compute the difference between two Date variables in a unit of time you specify. As with the Timer function, the difference between these two variables is the elapsed time. The following is a version of the Sleep subroutine shown earlier in this chapter, but this time it uses the Now and DateDiff functions:

Sub Sleep(lNumberOfSeconds As Long)

    Dim dateStart As Date

        ' record the start time

    dateStart = Now()

        'Spin until the time is up

    Do

        DoEvents

    Loop Until DateDiff("s", dateStart, Now()) >= lNumberOfSeconds

End Sub

The preceding loop is much simpler than the Sleep subroutine used earlier in this chapter—it needs no adjustment for passing midnight. And because the example provides an S as the first parameter to the DateDiff function, the function simply returns the number of seconds between the two dates—even if the number of seconds gets ridiculously large!

There are some drawbacks to this implementation:


Note

DateDiff is a particularly handy function when dealing with dates in your program. Its basic function is implied by its name—it simply returns the difference between two dates. How that difference is computed depends on the first parameter to the function. That parameter is a string representing the unit of time in which you want to measure the difference. In the preceding example, we used an S to state the difference between the two dates in seconds. Other parameters include D to state the difference in days, M for months, and YYYY for years. There are still more parameters possible; look in the Visual Basic help for a complete list.

Based on these limitations, a Timer-based solution is probably the better choice unless you need to time some very long events.

Responding to Time

People are mostly time driven. We wake up, go to work, go home, and go to bed at regular times. In other words, we respond to time-based events. Some programs must function the same way to be useful. A scheduler program is a classic example of this type of program; something as simple as an animated picture in a screen saver has similar requirements. That is, they both must do something at a predetermined time or interval. Fortunately, Windows provides a set of built-in timers you can use to create this type of program.

Windows timers can be thought of as an old-fashioned grandfather clock. This type of clock chimes at regular intervals, in most cases on the hour and half-hour. Windows timers work similarly, except that you can set the "chime" interval to any value you want, and instead of an audible bell tone, you get a "Timer Event." To access built-in Windows timers in Visual Basic code, simply place a Timer control on your form.

You place a Timer control on a form in the same way as any other control: select the Timer control icon from the toolbar and place it on a form. Don't worry about the size and placement of the control; the Timer control automatically assumes a default size and becomes invisible at runtime. Figure 4.3 shows the Timer control in the toolbar and on a form.


Figure 4.3. Using the Timer control.

Timer controls have very few properties compared to other Visual Basic controls. The following chart gives a rundown on the two most important ones:

Property


Description


Enabled

Essentially turns the timer on and off. Set this property to TRUE if you want to receive Timer events (that is, if you want to start the timer); set this to FALSE if you don't.

Interval

Controls how often Timer events occur. Set this to the amount of time in milliseconds (1/1000ths of a second) you want between Timer events. Note that this property is ignored until Enabled is set to TRUE.

The Top and Left properties have no bearing on the operation of the Timer control because the control is be invisible at runtime and can't receive any mouse or keyboard events. The best way to position a Timer control is to move it to some unused part of the form and leave it (its position really doesn't matter and you might as well have it out of the way).

A Timer control has only one event associated with it: the Timer event. This event is triggered at each passing of the specified Interval, if the Timer control is Enabled. By placing code in the Timer event subroutine of the Timer control, you can cause something to happen on each Interval.

Understanding Simple Program Animation

One of the most basic uses of a timer is to add simple animation to a window. A common business use of animation is to get an operator's or user's attention during the run of a long process. Listing 4.2 uses the Timer control to flash the border of a window using the Windows SDK FlashWindow function.

Option Explicit

' declarations differ between Windows 3.1 and Win32.

#If Win16 Then

    Private Declare Function FlashWindow Lib "User" (ByVal hWnd As Integer, ByVal _bInvert As Integer) As Integer

#Else

    Private Declare Function FlashWindow Lib "User32" (ByVal hWnd As Long, ByVal _bInvert As Long) As Long

#End If

Private Sub cbFlash_Click()

    timerFlasher.Interval = 500

    timerFlasher.Enabled = True

End Sub

Private Sub cbStop_Click()

    Dim iRet As Integer

        ' stop the flashing

    timerFlasher.Enabled = False

        ' Restore the window to its proper state

    iRet = FlashWindow(Me.hWnd, False)

End Sub

Private Sub Form_Load()

        ' make sure the timer isn't running when the program loads.

    timerFlasher.Enabled = False

End Sub

Private Sub timerFlasher_Timer()

    Dim iRet As Integer

        ' make the window flash

    iRet = FlashWindow(Me.hWnd, True)

End Sub

The FlashWindow function simply toggles the specified window's title bar and border between the Active and Inactive colors specified in the Control Panel. The hWnd parameter is the internal Windows handle for the window to be flashed. You can retrieve this value from the hWnd property on the Visual Basic Form object. When the next parameter is TRUE, it tells FlashWindow to toggle the window's title and border color, either from Active to Inactive or vice versa. If this parameter is FALSE, it returns the window to whatever color is appropriate for its current state—Active if the window is active or Inactive if the window is not active.

You'll notice the Conditional Compilation #IF...THEN that is around the declaration for the FlashWindow function. In fact, there are two declarations for FlashWindow: one that references the User library and one that references User32. This section illustrates a point about programming for multiple platforms. The first declaration of the FlashWindow function is for Windows 3.x—the 16-bit version of Windows. The second is for the 32-bit Windows versions—Windows 95 and Windows NT. You need two different declarations because Windows needs two versions of that module: one for 16-bit applications and one for 32-bit. Thus, Microsoft named them differently to prevent confusion.

Another difference between the two declarations is in the types of the parameters passed to and returned from FlashWindow. The parameters for the 32-bit versions are now Long variables instead of Integers. This is because when a machine is running a 32-bit operating system, its word size becomes a 32-bit value. Thus, the parameters are now Longs instead of Integers because an Integer is only 16-bits.

The Click event for the cbFlash button turns on the Timer control and sets the proper Interval for the flash (in this case, 500 milliseconds, or 1/2 second). The Click event for the cbStop button first shuts off the Timer and then returns the window to its proper state.

You can see that having the window flash on a Timer event doesn't keep the window from responding to other events, such as the click of a button. This kind of window is ideal for operator notification in a computer room, especially when the computer's monitor may be sitting in a row with several others. To be even more attention grabbing (or annoying), place a call to the Beep function in the Timer event.

A few caveats about the Timer control:

Figure 4.4 shows the variability in the regularity of the Timer event.


Figure 4.4. Demonstrating Timer variability.

All the example program in Figure 4.4 does is record the elapsed time between Timer events using the Timer function. When you run this program, it prints the time in seconds that elapsed between the scheduled Timer events.

Even when the system is idling, events don't fall exactly on the scheduled time. Try running another program while looking at this display. Notice that more than the desired Interval time may pass between Timer events. This can cause some complications, especially if you are writing a program that depends on very regular Intervals. If that is a requirement, your only alternative is to time with a loop based on the Timer function.

Waiting for Something

Another common use for Timer controls is to schedule applications. Such scheduler programs wait silently until there is something to do. Usually, these programs wake up at a particular time, but some look for other things such as the existence of a file.

The simplest version of this process is shown in the following example:

Do Until iDone

    DoEvents             ' let other events process

    IsThereAnythingToDo  ' check to see if there is anything to do

Loop

This function works but it has some drawbacks. It is a real time waster. This loop always spins, regardless of whether or not there is anything to do—even if it just checked half a millisecond ago. Although the DoEvents function ensures that other things can happen while this loop is spinning, all the bookkeeping and event checking will be a drag to all other processes in the system.

It usually isn't necessary to check continuously to see whether there is something to do. Checking at a longer interval, perhaps once a second or once a minute, is usually fine. Following is a revised version of the preceding scheduler loop that checks for new events only once every second:

Do Until iDone

    DoEvents             ' let other events process

    IsThereAnythingToDo  ' check to see if there is anything to do

    Sleep 1              ' snooze for a second

Loop

This example doesn't check to see whether there is anything to do quite so often and is less of a drag on the system's performance. However, the Sleep subroutine still has a loop that spins, with all the bookkeeping overhead involved. Other operating systems, such as UNIX, have a built-in Sleep function that places a program in a special "suspended" state for a period of time. While asleep, the operating system gives the program very little processor time until the specified duration has elapsed. Windows has no such function. The Timer control comes the closest to providing this capability: your application doesn't have to have any code running to check the time; all it has to do is sit back and wait for the Timer event to occur.

The implementation of a scheduler with the Timer control is a little different than the two non-Timer control implementations just shown. Instead of a loop, add code in the Timer event of the Timer control on the form, like this:

Private Sub timerScheduler_Timer()

    IsThereAnythingToDo    ' check to see if there is anything to do

End Sub

In the preceding example, there is no loop, no Sleep to delay, and no DoEvents call. Your program doesn't have to loop because the code in this event is executed on each passing of the specified Interval. Your program also doesn't have to delay itself to pace the number of checks it performs because the Timer control goes back to sleep when you return from this subroutine. Finally, your program doesn't need a call to DoEvents because, when you exit this subroutine, you automatically return control to Windows.

A Sample Scheduler Program

With the addition of a little code to build a list of things to schedule, you can have a basic scheduling program, like the one shown in Listing 4.3. Figure 4.5 shows the controls on the programs form.


Figure 4.5. The form for the scheduler example in Listing 4.3.

Option Explicit

Const SCHEDULE_DATA_FILE = "schedule.dat"

Private Type SCHEDULE_ITEM_TYPE         ' set up a structure to hold the schedule _entries.

    dteTime As Date

    sCommand As String

End Type

Dim strItems() As SCHEDULE_ITEM_TYPE    ' items in the schedule

Dim iCount As Integer

Dim dteLastTimeRun As Date

Private Sub cbClose_Click()

    End     ' end this program

End Sub

Private Sub Form_Load()

    If Not ReadScheduledItems() Then    ' fill the schedule array

        End

    End If

    dteLastTimeRun = Time

    timerScheduler.Enabled = True       ' start the timer

End Sub

Private Sub timerScheduler_Timer()

    PrintTime               ' Print the current time on the window

    IsThereAnythingToDo     ' Check the scheduled list

End Sub

Private Function ReadScheduledItems() As Integer

    Dim sFileName As String

    Dim iFile As Integer

    On Error GoTo Error_ReadScheduledItems

        ' Open the schedule file, assume that it is in the app.path

    sFileName = App.Path & "\" & SCHEDULE_DATA_FILE

    iFile = FreeFile

    Open sFileName For Input As #iFile

    iCount = 0

    Do While Not EOF(iFile)

        iCount = iCount + 1

            ' expand the structure to hold the additional item

        ReDim Preserve strItems(iCount)

            ' read it into the structure

        Input #iFile, strItems(iCount).dteTime, strItems(iCount).sCommand

    Loop

    Close #iFile

    ReadScheduledItems = True

    Exit Function

Error_ReadScheduledItems:

    ReadScheduledItems = False

    Exit Function

End Function

Private Sub IsThereAnythingToDo()

    Dim i As Integer, iRet As Integer

    Dim dteCurrentTime As Date

    On Error Resume Next

    dteCurrentTime = Time

        ' run through the array of things to do

    For i = 1 To iCount

            ' If the scheduled start time is between the time of the last run

            ' and the current time, then it is time to start the specified

            ' program.

        If strItems(i).dteTime > dteLastTimeRun And strItems(i).dteTime <= _dteCurrentTime Then

            iRet = Shell(strItems(i).sCommand, 1)   ' run the program

            If Err <> 0 Then

                MsgBox "Error running: " & strItems(i).sCommand

            End If

        End If

    Next

    dteLastTimeRun = dteCurrentTime

End Sub

Private Sub PrintTime()

    Dim sTime As String

        ' only print the time if the the window is not minimized

    If WindowState = 0 Then

        Me.Cls

        sTime = Format$(Time, "Long Time")                      ' format the time

        Me.CurrentX = (Me.ScaleWidth - Me.TextWidth(sTime)) / 2 ' center the time

        Me.CurrentY = Me.TextHeight(sTime) / 2

        Me.Print sTime                                          ' and print it.

    End If

End Sub

This example is a basic scheduler that reads a list of scheduled commands from a file and executes them at the appointed time. The user interface is simple; the program prints the current time on each Timer event and provides only a Close button that ends the program.

When the form loads, the program reads the items to be run from the file using the ReadScheduledItems function; it then starts the timer by enabling the Timer control. The only purpose for the cbClose_Click() subroutine is to force the program to end.

The format of the file that contains the items to be scheduled is also very simple—it was designed to be read quickly by Visual Basic's Input statement. Each line describes a schedule entry. The first parameter for each line is the time at which the command should be executed; the second parameter is a string that contains the command to be executed. An example of this file is given here:

#09:15:00 pm#, "calc.exe"

#09:17:00 pm#, "notepad.exe"

#09:20:00 pm#, "wordpad.exe"

The memory management in the ReadScheduledItems subroutine is not the best example of how to read information into a dynamic array. Calling ReDim for each element being read is slow and can cause fragmentation in the pool of memory used by Visual Basic. A better solution is to expand the array in fixed-sized extents, perhaps 10 at a time. This arrangement prevents you from going back to Windows for more memory on each iteration and eliminates a lot of the moving of data that goes on with the ReDim Preserve statement.

The heart of the scheduling engine is in the Timer event subroutine, timerScheduler_Timer(). This subroutine simply prints the current time on the form using the PrintTime subroutine and then calls the IsThereAnythingToDo subroutine, which actually runs any scheduled event whose time has come:

How to Determine When to Run an Item

Determining when it is time to run an item is more complex than it seems. You can't simply use the following logic to compare to see whether the desired runtime is greater than the current time:

If Time > strItems(i).dteTime Then

The problem with this test is that the condition is true not only at the time the item is to be run, but also every time after that, until midnight. The command in question runs at the appointed time and at every timer interval thereafter!

The simplest way to determine when a program must be run is to see whether the desired runtime has passed between the last Timer event and the current one. To do this, store the time of the last Timer event so that you know the lower bound of the time span between the clicks. To see whether a program must be run, simply see whether it falls between the last Timer event and the current one:

If strItems(i).dteTime > dteLastTimeRun And strItems(i).dteTime <= dteCurrentTime Then

The basic formula for determining whether or not an item must be run is LastTimeRun > DesiredRunTime >= CurrentTime. Note that one of the comparison operators in the preceding If statement is a "less than or equal to" operator (<=). This operator is used to accommodate the odd occasion on which a Timer event falls exactly on the scheduled time for an item. If you don't use this operator, events that fall exactly on the time of the Timer event are not executed.

Also note that you must store the real time (using the Timer function) of both the last Timer event and the current one; you cannot simply assume that just the specified Interval has passed. This arrangement compensates for the fact that Timer events don't always come at exactly the specified intervals, as demonstrated earlier in this chapter.

The Timer Function versus the Timer Control

In addition to sharing the same name, the Timer function and the Timer control also have a lot of overlap in their functionality. They both deal with time, and to some degree, can be used to replace each other.

As described earlier in this chapter, delay loops based on the Timer function can be used to schedule things. They aren't always the best solution, however, because they require a part of your program to always be spinning while waiting for the next scheduled time. But these delay loops are serviceable and don't depend on getting a Timer event from Windows. They are also reasonably accurate (as accurate as anything is in a multitasking operating system).

What may not be obvious is that the Timer control can be used to help in measuring time. By nature, the Timer control is not very good at fine time measurements (remember that it depends on your program being in a state to accept messages to work), but it is good for measuring gross time between parts of an application.

An example of a common use of a Timer control to measure time is in some of the popular disk backup programs that measure the total time the backup took; these programs also split out the amount of time the program took to actually perform the backup versus the time it spent waiting for the user. The following code fragment demonstrates one way to implement this type of timer measurement:

Dim sngLastTime As Single        ' the last time we record the time, for bookkeeping

Dim sngUserTime As Single        ' a bucket to hold the time spent waiting for user response

Dim sngProgramTime As Single     ' a bucket to hold the time our program has spent running

Private Sub Timer1_Timer()

    Dim sngElapsedTime As Single, sngCurrentTime As Single

    sngCurrentTime = Timer

        ' decide where to allocate this time

    If iWaitingOnUser Then

        sngUserTime = sngCurrentTime - sngLastTime

    Else

        sngProgramTime = sngCurrentTime - sngLastTime

    End If

    PrintTimes

    sngLastTime = sngCurrentTime

End Sub

All this code does is add the elapsed time since the last Timer event to either user time or program time, based on the iWaitingOnUser flag. By simply flipping the iWaitingOnUser flag, you can allocate time to both measurements without adding a lot of code in the application's main logic.


Note

The measured time, and its allocation between user and nonuser time, is far from exact because the part of the process (indicated by the iWaitingOnUser flag) that happens to be running when the Timer event hits gets credit for the entire elapsed time since the last Timer event. However, absolute accuracy is not normally a requirement for these types of timers because the elapsed time is simply a gross measurement for the user. This implementation should do fine for most applications.

Note that you still use the Timer function to actually measure the elapsed time. Again, this is because we can't depend on the Timer events to match exactly (or even come close to) the desired intervals.

Summary

The ability to measure and respond to time is an important part of any programming environment. Although it is not a very good choice for rigorous real-time applications, Visual Basic 4.0 provides some handy and easy-to-use tools for time management.

To measure time, Visual Basic 4.0 provides the Timer function to measure time in raw seconds. The Timer function is the basis for nearly all time measurement in Visual Basic. When you have to measure larger amounts of time—even potentially a number of days or weeks—Visual Basic provides the Now function.

To respond to time, Visual Basic 4.0 provides the Timer control, which lets your Visual Basic applications tap into the power of Window's timer capability and write your own animated or scheduling programs.

Previous Page TOC Next Page